
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: ArrayField.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;

import org.dellroad.sidekar.util.PackedLong;
import org.dellroad.stuff.java.Primitive;
import org.dellroad.stuff.java.PrimitiveSwitch;
import org.dellroad.stuff.string.ByteArrayEncoder;
import org.dellroad.stuff.string.ParseContext;

/**
 * Array field.
 *
 * <p>
 * An array field can have any {@link AtomicField} as its element type.
 * Array field values are deeply copied whenever they are read or written, so
 * for example, reading an array and then changing one element of the returned
 * value will not affect the original array field.
 *
 * <p>
 * Array fields may be {@code null}, which is their default value.
 *
 * @param <E> Java type used to represent one element of this field's array value
 */
public final class ArrayField<E> extends AtomicField<Object> {

    // Note: this class does some funky casting to work around generics problems
    // caused by the fact that primitive arrays are not instances of Object[].

    private final AtomicField<E> elemField;
    private final Primitive primitive;

    /**
     * Constructor.
     */
    @SuppressWarnings({ "unchecked" })
    ArrayField(String name, Entity<?> entity, AtomicField<E> elemField) {
        super(name, entity, (Class<Object>)(Object)getArrayType(elemField.getType()));
        this.elemField = elemField;
        this.primitive = Primitive.get(this.elemField.getType());
    }

    /**
     * Get the field type that corresponds to this array's element type.
     *
     * <p>
     * The returned field will have no name associated with it and cannot be used
     * to set any values.
     */
    public AtomicField<E> getElementField() {
        return this.elemField;
    }

    @Override
    public boolean equals(Object array1, Object array2) {
        if (this.primitive != null)
            return this.primitive.visit(new PrimitiveEqualsSwitch(array1, array2));
        return Arrays.deepEquals((Object[])array1, (Object[])array2);
    }

    @Override
    public void visit(FieldSwitch fieldSwitch) {
        fieldSwitch.caseArrayField(this);
    }

    @Override
    public Object decode(DataInputStream input) throws IOException {

        // Read length
        int length = (int)PackedLong.read(input);
        if (length == -1)
            return null;
        assert length >= 0;

        // Handle primitive array cases
        if (this.primitive != null) {
            try {
                return this.primitive.visit(new PrimitiveArrayReader(input, length));
            } catch (RuntimeException e) {
                if (e.getCause() instanceof IOException)
                    throw (IOException)e.getCause();
                throw e;
            }
        }

        // Handle object array cases
        Object[] array = (Object[])Array.newInstance(this.elemField.getRuntimeType(), length);
        for (int i = 0; i < length; i++)
            array[i] = this.elemField.decode(input);
        return array;
    }

    @Override
    @SuppressWarnings({ "unchecked" })
    public void encode(DataOutputStream output, Object value) throws IOException {

        // Write length
        if (value == null) {
            PackedLong.write(output, -1);
            return;
        }
        int length = Array.getLength(value);
        PackedLong.write(output, length);

        // Handle primitive array cases
        if (this.primitive != null) {
            try {
                this.primitive.visit(new PrimitiveArrayWriter(output, value));
            } catch (RuntimeException e) {
                if (e.getCause() instanceof IOException)
                    throw (IOException)e.getCause();
                throw e;
            }
            return;
        }

        // Handle object array cases
        Object[] array = (Object[])value;
        for (int i = 0; i < length; i++)
            this.elemField.encode(output, (E)array[i]);
    }

    @Override
    public String toString(Object value) {

        // Handle null
        if (value == null)
            return "null";

        // Special case byte[] arrays
        StringBuilder buf = new StringBuilder();
        if (Primitive.BYTE.equals(this.primitive)) {
            buf.append('[');
            buf.append(ByteArrayEncoder.encode((byte[])value));
            buf.append(']');
            return buf.toString();
        }

        // Handle all other cases
        buf.append('{');
        final int length = Array.getLength(value);
        for (int i = 0; i < length; i++) {
            buf.append(i > 0 ? ", " : " ");
            buf.append(this.elemField.toString(this.elemField.cast(Array.get(value, i))));
        }
        buf.append(" }");
        return buf.toString();
    }

    @Override
    public Object fromString(ParseContext ctx) {

        // Handle null
        if (ctx.tryLiteral("null"))
            return null;

        // Skip initial whitespace
        ctx.skipWhitespace();

        // Special case byte[] arrays; otherwise decode normal array
        switch (ctx.peek()) {
        case '[':
            ctx.read();
            if (!Primitive.BYTE.equals(this.primitive))
                throw new IllegalArgumentException("can't decode byte[] into a non-byte[] array");
            byte[] byteArray = ByteArrayEncoder.decode(ctx.matchPrefix("[\\p{XDigit}\\s]*").group());
            ctx.expect(']');
            return byteArray;
        case '{':
            ctx.read();
            ArrayList<Object> list = new ArrayList<Object>();
            while (true) {
                ctx.skipWhitespace();
                if (ctx.tryLiteral("}"))
                    break;
                if (!list.isEmpty()) {
                    ctx.expect(',');
                    ctx.skipWhitespace();
                }
                list.add(this.elemField.fromString(ctx));
            }
            Object array = this.primitive != null ?
                Array.newInstance(this.primitive.getType(), list.size()) :
                Array.newInstance(this.elemField.getType(), list.size());
            for (int i = 0; i < list.size(); i++)
                Array.set(array, i, list.get(i));
            return array;
        default:
            throw ctx.buildException("expected `[' or `{`");
        }
    }

    /**
     * Get the Java array class whose element type is the java type associated with the given field.
     * If the given class is a primitive wrapper class, this will return the corresponding primitive array type
     * (not the corresponding primitive wrapper array type).
     */
    @SuppressWarnings({ "unchecked" })
    public static <R> Class<R[]> getArrayType(Class<R> elemType) {
        String arrayName;
        Primitive p = Primitive.get(elemType);
        if (p != null)
            arrayName = "[" + p.getLetter();
        else if (elemType.getName().charAt(0) == '[')
            arrayName = "[" + elemType.getName();
        else
            arrayName = "[L" + elemType.getName() + ";";
        try {
            return (Class<R[]>)Class.forName(arrayName, false, elemType.getClassLoader());
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("internal error", e);
        }
    }

    // Class to read a one-dimensional primitive array's elements
    static final class PrimitiveArrayReader extends IOPrimitiveSwitch<Object> {

        private final DataInputStream input;
        private final int length;

        PrimitiveArrayReader(DataInputStream input, int length) {
            this.input = input;
            this.length = length;
        }

        @Override
        public Object caseIOBoolean() throws IOException {
            byte[] buf = new byte[(this.length + 7) / 8];
            this.input.readFully(buf);
            boolean[] data = new boolean[this.length];
            for (int i = 0; i < this.length; i++)
                data[i] = (buf[i / 8] & (1 << (i % 8))) != 0;
            return data;
        }

        @Override
        public Object caseIOByte() throws IOException {
            byte[] data = new byte[this.length];
            this.input.readFully(data);
            return data;
        }

        @Override
        public Object caseIOCharacter() throws IOException {
            char[] data = this.input.readUTF().toCharArray();
            if (data.length != this.length) {
                throw new IllegalArgumentException("encoded UTF-8 length "
                  + data.length + " does not match array length " + this.length);
            }
            return data;
        }

        @Override
        public Object caseIOShort() throws IOException {
            short[] data = new short[this.length];
            for (int i = 0; i < this.length; i++) {
                long value = PackedLong.read(this.input);
                if (value != (short)value)
                    throw new IllegalArgumentException("invalid encoded short value at index " + i);
                data[i] = (short)value;
            }
            return data;
        }

        @Override
        public Object caseIOInteger() throws IOException {
            int[] data = new int[this.length];
            for (int i = 0; i < this.length; i++) {
                long value = PackedLong.read(this.input);
                if (value != (int)value)
                    throw new IllegalArgumentException("invalid encoded integer value at index " + i);
                data[i] = (int)value;
            }
            return data;
        }

        @Override
        public Object caseIOFloat() throws IOException {
            float[] data = new float[this.length];
            for (int i = 0; i < this.length; i++) {
                long value = PackedLong.read(this.input);
                if (value != (int)value)
                    throw new IllegalArgumentException("invalid encoded float value at index " + i);
                data[i] = Float.intBitsToFloat((int)value);
            }
            return data;
        }

        @Override
        public Object caseIOLong() throws IOException {
            long[] data = new long[this.length];
            for (int i = 0; i < this.length; i++)
                data[i] = PackedLong.read(this.input);
            return data;
        }

        @Override
        public Object caseIODouble() throws IOException {
            double[] data = new double[this.length];
            for (int i = 0; i < this.length; i++) {
                long value = PackedLong.read(this.input);
                data[i] = Double.longBitsToDouble(value);
            }
            return data;
        }
    }

    // Class to write a one-dimensional primitive array's elements
    static final class PrimitiveArrayWriter extends IOPrimitiveSwitch<Void> {

        private final DataOutputStream output;
        private final Object array;

        PrimitiveArrayWriter(DataOutputStream output, Object array) {
            assert array != null;
            this.output = output;
            this.array = array;
        }

        @Override
        public Void caseIOBoolean() throws IOException {
            boolean[] data = (boolean[])this.array;
            byte[] buf = new byte[(data.length + 7) / 8];
            for (int i = 0; i < data.length / 8; i++) {
                int offset = i * 8;
                buf[i] = (byte)(
                    (data[offset + 0] ? 0x01 : 0)
                  | (data[offset + 1] ? 0x02 : 0)
                  | (data[offset + 2] ? 0x04 : 0)
                  | (data[offset + 3] ? 0x08 : 0)
                  | (data[offset + 4] ? 0x10 : 0)
                  | (data[offset + 5] ? 0x20 : 0)
                  | (data[offset + 6] ? 0x40 : 0)
                  | (data[offset + 7] ? 0x80 : 0));
            }
            for (int i = data.length & ~7; i < data.length; i++) {
                if (data[i])
                    buf[i / 8] |= 1 << (i % 8);
            }
            this.output.write(buf, 0, buf.length);
            return null;
        }

        @Override
        public Void caseIOByte() throws IOException {
            byte[] data = (byte[])this.array;
            this.output.write(data, 0, data.length);
            return null;
        }

        @Override
        public Void caseIOCharacter() throws IOException {
            char[] data = (char[])this.array;
            this.output.writeUTF(new String(data));
            return null;
        }

        @Override
        public Void caseIOShort() throws IOException {
            short[] data = (short[])this.array;
            for (int i = 0; i < data.length; i++)
                PackedLong.write(this.output, data[i]);
            return null;
        }

        @Override
        public Void caseIOInteger() throws IOException {
            int[] data = (int[])this.array;
            for (int i = 0; i < data.length; i++)
                PackedLong.write(this.output, data[i]);
            return null;
        }

        @Override
        public Void caseIOFloat() throws IOException {
            float[] data = (float[])this.array;
            for (int i = 0; i < data.length; i++)
                PackedLong.write(this.output, Float.floatToRawIntBits(data[i]));
            return null;
        }

        @Override
        public Void caseIOLong() throws IOException {
            long[] data = (long[])this.array;
            for (int i = 0; i < data.length; i++)
                PackedLong.write(this.output, Double.doubleToRawLongBits(data[i]));
            return null;
        }

        @Override
        public Void caseIODouble() throws IOException {
            double[] data = (double[])this.array;
            for (int i = 0; i < data.length; i++)
                PackedLong.write(this.output, Double.doubleToRawLongBits(data[i]));
            return null;
        }
    }

    // Class to compare primitive arrays for equality
    static final class PrimitiveEqualsSwitch implements PrimitiveSwitch<Boolean> {

        private final Object array1;
        private final Object array2;

        public PrimitiveEqualsSwitch(Object array1, Object array2) {
            this.array1 = array1;
            this.array2 = array2;
        }

        @Override
        public Boolean caseBoolean() {
            return Arrays.equals((boolean[])array1, (boolean[])array2);
        }

        @Override
        public Boolean caseByte() {
            return Arrays.equals((byte[])array1, (byte[])array2);
        }

        @Override
        public Boolean caseCharacter() {
            return Arrays.equals((char[])array1, (char[])array2);
        }

        @Override
        public Boolean caseShort() {
            return Arrays.equals((short[])array1, (short[])array2);
        }

        @Override
        public Boolean caseInteger() {
            return Arrays.equals((int[])array1, (int[])array2);
        }

        @Override
        public Boolean caseFloat() {
            return Arrays.equals((float[])array1, (float[])array2);
        }

        @Override
        public Boolean caseLong() {
            return Arrays.equals((long[])array1, (long[])array2);
        }

        @Override
        public Boolean caseDouble() {
            return Arrays.equals((double[])array1, (double[])array2);
        }
    }

    // PrimitiveSwitch that allows each method to throw IOExceptions
    static abstract class IOPrimitiveSwitch<R> implements PrimitiveSwitch<R> {

        @Override
        public final R caseBoolean() {
            try {
                return this.caseIOBoolean();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOBoolean() throws IOException;

        @Override
        public final R caseByte() {
            try {
                return this.caseIOByte();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOByte() throws IOException;

        @Override
        public final R caseCharacter() {
            try {
                return this.caseIOCharacter();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOCharacter() throws IOException;

        @Override
        public final R caseShort() {
            try {
                return this.caseIOShort();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOShort() throws IOException;

        @Override
        public final R caseInteger() {
            try {
                return this.caseIOInteger();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOInteger() throws IOException;

        @Override
        public final R caseFloat() {
            try {
                return this.caseIOFloat();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOFloat() throws IOException;

        @Override
        public final R caseLong() {
            try {
                return this.caseIOLong();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIOLong() throws IOException;

        @Override
        public final R caseDouble() {
            try {
                return this.caseIODouble();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        protected abstract R caseIODouble() throws IOException;
    }
}

